﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using SlimDX;
using SlimDX.Direct3D9;

using System.Windows.Forms;
using System.Runtime.Serialization;
using System.Security.Permissions;
using System.IO;
using System.Xml;
using System.Xml.Serialization;

namespace Splino.D3D
{
    /// <summary>
    /// The base class for all Spline Objects, which contain a list
    /// of Point Objects representing the spline's control points
    /// and a virtual evaluate function that defines the type of spline
    /// </summary>
    [Serializable()]
    public class SplineObject : SplinoObject
    {
        /// <summary>
        /// True if the spline's first visible ontrol point is the same as its
        /// last visible point
        /// </summary>
        protected bool _closed = false;
        /// <summary>
        /// Gets or sets  whether the spline's first visible control point is the same as
        /// its last visible point
        /// </summary>
        public virtual bool Closed
        {
            get { return _closed; }
            set { _closed = value; }
        }

        /// <summary>
        /// The last point of the spline
        /// </summary>
        [XmlIgnore()]
        protected PointObject _endPoint;
        /// <summary>
        /// Gets the last point of the spline
        /// </summary>
        public PointObject EndPoint
        {
            get { return _endPoint; }
        }

        /// <summary>
        /// The point the mouse has been snapped to close the spline, or null otherwise
        /// </summary>
        protected ObjectSnap _activeCloseSplineSnap;
        /// <summary>
        /// Gets the point the mouse has been snapped to close the spline, or null otherwise
        /// </summary>
        [XmlIgnore()]
        public ObjectSnap ActiveCloseSplineSnap { get { return _activeCloseSplineSnap; } }

        /// <summary>
        /// A protected constructor so that this class can not be used
        /// directly, instead initialize an inherited form
        /// </summary>
        protected SplineObject()
            : base()
        {
            _type = SplinoObjectType.Spline;
            _color = Palette.SplineObject;
        }

        /// <summary>
        /// Protected constructor used for creating Point Objects from
        /// clipboard data objects
        /// </summary>
        /// <param name="info">Clipboard Serialization Info</param>
        /// <param name="context">Clipboard Streaming Context</param>
        protected SplineObject(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
            _type = SplinoObjectType.Spline;
        }

        /// <summary>
        /// Serializes the point for use in a clipboard data object for copy
        /// and paste operations
        /// </summary>
        /// <param name="serializationInfo">Clipboard Serialization Info</param>
        /// <param name="streamingContext">Clipboard Streaming Context</param>
        [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
        public override void GetObjectData(SerializationInfo serializationInfo,
            StreamingContext streamingContext)
        {
            base.GetObjectData(serializationInfo, streamingContext);

            foreach (PointObject controlPoint in _sceneChildren)
                controlPoint.Hide();
        }

        /// <summary>
        /// True if the spline's control points are hidden from the scene
        /// </summary>
        protected bool _controlPointsHidden = true;
        /// <summary>
        /// Gets if the spline's control points are hidden from the scene
        /// </summary>
        public bool ControlPointsHidden
        {
            get { return _controlPointsHidden; }
            set
            {
                _controlPointsHidden = value;
                if (value)
                    HideControlPoints();
                else
                    ShowControlPoints();
            }
        }

        /// <summary>
        /// Returns a Vector3 point on the spline at a specified index
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public virtual Vector3 Evaluate(float index) { return new Vector3(0f, 0f, 0f); }

        /// <summary>
        /// <para>Tests the object to see if any part of it is clipped by the selection
        /// rectangle in a given Splino Viewport, returning true if the object
        /// was clipped by the rectangle (or completely contained by), and false
        /// if it is not.  Used for multiple object picking/selection</para>
        /// <para>To exclusively select a spline, all control points must be in
        /// the viewport's viewing fustrum</para>
        /// </summary>
        /// <param name="splinoViewport">The viewport containing the selection
        /// rectangle</param>
        /// <returns>True if the object was clipped by the selection rectangle,
        /// false if it was not</returns>
        public override List<SplinoObject> IntersectExclusive(SplinoViewport splinoViewport)
        {
            bool allControlPointsInFustrum = true;

            List<SplinoObject> splinoObjects = new List<SplinoObject>();
            splinoObjects.AddRange(base.IntersectExclusive(splinoViewport));

            foreach (PointObject controlPoint in _sceneChildren)
            {
                if (!splinoViewport.PointInFustrum(controlPoint.Center))
                    allControlPointsInFustrum = false;
            }

            if (allControlPointsInFustrum)
                splinoObjects.Add(this);

            return splinoObjects;
        }

        /// <summary>
        /// Tests the object to see if any part of it is clipped by the selection
        /// rectangle in a given Splino Viewport, returning true if the object
        /// was clipped by the rectangle (or completely contained by), and false
        /// if it is not.  Used for multiple object picking/selection
        /// </summary>
        /// <param name="splinoViewport">The viewport containing the selection
        /// rectangle</param>
        /// <returns>True if the object was clipped by the selection rectangle,
        /// false if it was not</returns>
        public override List<SplinoObject> IntersectInclusive(SplinoViewport splinoViewport)
        {
            bool objectInFustrum = false;

            List<SplinoObject> splinoObjects = new List<SplinoObject>();
            splinoObjects.AddRange(base.IntersectInclusive(splinoViewport));

            foreach (SplinoObject splinoObject in _sceneChildren)
            {
                PointObject controlPoint = (PointObject)splinoObject;
                if (splinoViewport.PointInFustrum(controlPoint.Center))
                    objectInFustrum = true;
            }

            float intersectInformation;
            Mesh selectionFustrumMesh = splinoViewport.SelectionFustrumMesh();
            for (int i = 0; i < _vertexList.Length - 1; i++)
            {
                if (selectionFustrumMesh.Intersects(
                    new Ray(_vertexList[i].Position,
                    _vertexList[i + 1].Position - _vertexList[i].Position),
                    out intersectInformation))
                    if (intersectInformation < 1)
                        objectInFustrum = true; ;
            }

            if (objectInFustrum)
                splinoObjects.Add(this);

            return splinoObjects;
        }

        /// <summary>
        /// Creates a closed spline with continuous end points
        /// </summary>
        public virtual void CloseSpline()
        {
            _closed = true;
        }

        /// <summary>
        /// Creates an open ended spline with two endpoints
        /// </summary>
        public virtual void OpenSpline()
        {
            _closed = false;
        }

        /// <summary>
        /// Adds an existing Point Object as a new control point to the 
        /// end of the spline
        /// </summary>
        /// <param name="point">The Point Object to append to the end
        /// of the spline</param>
        public virtual void AppendControlPoint(PointObject point)
        {
            AddSceneChild(point);
            _endPoint = point;
            UpdateAllVerticies();

            point.Transformed += new EventHandler(ControlPoint_Moved);
        }

        /// <summary>
        /// Removes the last control point of the spline
        /// </summary>
        public virtual void RemoveEndPoint()
        {
            _sceneChildren.Remove(_endPoint);

            RemoveSceneChild(_endPoint);
            _endPoint.Delete();
            _endPoint = (PointObject)_sceneChildren[_sceneChildren.Count - 1];
            UpdateAllVerticies();
        }

        /// <summary>
        /// Inserts a control point into the spline at the given index.
        /// </summary>
        /// <param name="index">The index of the scene children collection
        /// to insert the object into.</param>
        /// <param name="point">The control point to insert</param>
        public void InsertControlPoint(int index, PointObject point)
        {
            if (point.SceneParent != null)
                point.SceneParent.RemoveSceneChild(point);

            _sceneChildren.Insert(index, point);
            point.SceneParent = this;
            point.SceneParentName = this.Name;

            if (index == _sceneChildren.Count - 1)
                _endPoint = point;
            UpdateAllVerticies();

            point.Moved += new EventHandler(ControlPoint_Moved);
        }

        /// <summary>
        /// Inserts a control point, second to last in the scene children collection.
        /// </summary>
        /// <param name="point">The control point to insert.</param>
        public void InsertControlPointBeforeEnd(PointObject point)
        {
            if (_sceneChildren.Count > 0)
                InsertControlPoint(_sceneChildren.Count - 1, point);
            else
                AppendControlPoint(point);
        }

        /// <summary>
        /// Makes all control points of the spline visible
        /// </summary>
        public void ShowControlPoints()
        {
            foreach (SplinoObject controlPoint in _sceneChildren)
                controlPoint.Show();

            _controlPointsHidden = false;
            Lock();
        }

        /// <summary>
        /// Makes all control points of the spline invisible
        /// </summary>
        public void HideControlPoints()
        {
            foreach (SplinoObject controlPoint in _sceneChildren)
                controlPoint.Hide();

            _controlPointsHidden = true;
            Unlock();
        }

        /// <summary>
        /// Makes the object selectable and returns the object's
        /// normal drawing color
        /// </summary>
        public override void Unlock()
        {
            base.Unlock();
            if (!_controlPointsHidden)
                HideControlPoints();
        }

        /// <summary>
        /// Moves the Spline's end point to match the position
        /// of the mouse
        /// </summary>
        public void BindEndPointToDrawPoint()
        {
            if (!_closed)
            {
                _endPoint.Center = SceneManager.Instance.CursorPoint;
                //UpdateAllVerticies();
            }
        }

        /// <summary>
        /// Updates each vertex in the spline from the current position
        /// of each control point
        /// </summary>
        public virtual void UpdateAllVerticies()
        {
            _vertexCount = _sceneChildren.Count;

            _vertexList = new CustomVertex.PositionColored[_vertexCount];
            foreach (PointObject point in _sceneChildren)
            {
                _vertexList[_sceneChildren.IndexOf(point)].X =
                    point.Center.X;
                _vertexList[_sceneChildren.IndexOf(point)].Y =
                    point.Center.Y;
                _vertexList[_sceneChildren.IndexOf(point)].Z =
                    point.Center.Z;
                _vertexList[_sceneChildren.IndexOf(point)].Color =
                    _color.ToArgb();
            }

            Dispose();
            ResetBuffers();
        }

        protected virtual void ControlPoint_Moved(object sender, EventArgs e)
        {
            UpdateControlPoint((PointObject)sender);
        }

        /// <summary>
        /// Updates the position of the given control point
        /// </summary>
        /// <param name="controlPoint"></param>
        public virtual void UpdateControlPoint(PointObject controlPoint)
        {
            _vertexList[_sceneChildren.IndexOf(controlPoint)].X =
                controlPoint.Center.X;
            _vertexList[_sceneChildren.IndexOf(controlPoint)].Y =
                controlPoint.Center.Y;
            _vertexList[_sceneChildren.IndexOf(controlPoint)].Z =
                controlPoint.Center.Z;

            DataStream stream = _vertexBuffer.Lock(
                0,
                _vertexList.Length * CustomVertex.PositionColored.SizeInBytes,
                LockFlags.None);
            stream.WriteRange(_vertexList);
            _vertexBuffer.Unlock();
        }

        /// <summary>
        /// <para>Shoots a ray from a starting position vector and a unit vector
        /// direction, and tests to see if it intersects with this Splino Object,
        /// returning true if it does intersect and false if it does not.  Used
        /// for picking/selecting objects.</para>
        /// <para>For Point Objects, the ray is tested against a simple box
        /// mesh that is scaled according to UnitsPerPixel to keep the box
        /// the same relative size at all zoom levels</para>
        /// </summary>
        /// <param name="viewport">The viewport that contains the picking ray and 
        /// camera used for the picking test</param>
        /// <param name="rayPosition">The start position of the picking ray, generally
        /// the camera's location subtracted from an unprojected mouse click</param>
        /// <param name="rayDirection">A unit vector </param>
        /// <returns>True if the picking ray intersected the object, and false
        /// if it did not</returns>
        public override List<SplinoObject> IntersectRay(SplinoViewport viewport,
            Vector3 rayPosition, Vector3 rayDirection)
        {
            bool rayIntersected = false;
            List<SplinoObject> splinoObjects = new List<SplinoObject>();

            splinoObjects.AddRange(base.IntersectRay(viewport, rayPosition, rayDirection));

            Vector3[] vertices = new Vector3[_vertexList.Length];
            for (int i = 0; i < _vertexList.Length; i++)
                vertices[i] = _vertexList[i].Position;

            BoundingBox boundingBox = BoundingBox.FromPoints(vertices);

            float minPad = viewport.UnitsPerPixel(boundingBox.Minimum) * 5;
            Vector3 boundingBoxMin = boundingBox.Minimum + new Vector3(-minPad, -minPad, -minPad);
            float maxPad = viewport.UnitsPerPixel(boundingBox.Maximum) * 5;
            Vector3 boundingBoxMax = boundingBox.Maximum + new Vector3(maxPad, maxPad, maxPad);

            float intersection;

            if (!Ray.Intersects(
                new Ray(rayPosition, rayDirection),
                new BoundingBox(boundingBoxMin, boundingBoxMax),
                out intersection))
                return splinoObjects;

            for (int i = 0; i < _vertexList.Length - 1; i++)
            {
                float radius1 = viewport.UnitsPerPixel(_vertexList[i].Position) * 5;
                float radius2 = viewport.UnitsPerPixel(_vertexList[i + 1].Position) * 5;
                Vector3 segmentAxis = _vertexList[i + 1].Position - _vertexList[i].Position;
                float length = segmentAxis.Length();

                Mesh lineSegmentBoundingCylinder = Mesh.CreateCylinder(
                    DeviceManager.Instance.Device,
                    radius1,
                    radius2,
                    length,
                    3,
                    1);

                Vector3 transRayDirection = rayDirection;
                Vector3 transRayPosition = rayPosition;

                Vector3 segmentCenter = (_vertexList[i].Position + _vertexList[i + 1].Position) * 0.5f;
                transRayPosition -= segmentCenter;

                Vector3 v1 = Vector3.Normalize(segmentAxis);
                Vector3 v2 = new Vector3(0, 0, 1);
                Vector3 v3 = Vector3.Normalize(Vector3.Cross(v1, v2));
                Vector3 v4 = Vector3.Cross(v3, v1);

                if (v3.LengthSquared() > 0.9f)
                {
                    Matrix m1 = Matrix.Identity;
                    m1.M11 = v1.X;
                    m1.M12 = v1.Y;
                    m1.M13 = v1.Z;
                    m1.M21 = v4.X;
                    m1.M22 = v4.Y;
                    m1.M23 = v4.Z;
                    m1.M31 = v3.X;
                    m1.M32 = v3.Y;
                    m1.M33 = v3.Z;

                    float cos = Vector3.Dot(v2, v1);
                    float sin = Vector3.Dot(v2, v4);

                    Matrix m2 = Matrix.Identity;
                    m2.M11 = cos;
                    m2.M12 = sin;
                    m2.M21 = -sin;
                    m2.M22 = cos;

                    Matrix alignRay = Matrix.Invert(m1) * m2 * m1;
                    transRayDirection = Vector3.TransformCoordinate(transRayDirection, alignRay);
                    transRayPosition = Vector3.TransformCoordinate(transRayPosition, alignRay);
                }

                if (lineSegmentBoundingCylinder.Intersects(new Ray(transRayPosition, transRayDirection)))
                {
                    lineSegmentBoundingCylinder.Dispose();
                    rayIntersected = true;
                }
                lineSegmentBoundingCylinder.Dispose();
            }
            if (rayIntersected)
                splinoObjects.Add(this);

            return splinoObjects;
        }

        /// <summary>
        /// <para>Creates a unique name for the Splino Object, generates a ListViewItem
        /// based off of its properties for display in the parent object's children
        /// list, and associates the object with its parent</para>
        /// <para>For Spline Objects, the verticies are updated</para>
        /// </summary>
        /// <param name="parentObject">The parent Splino Object</param>
        public override void Initialize(SplinoObject parentObject)
        {
            base.Initialize(parentObject);

            UpdateAllVerticies();
        }

        /// <summary>
        /// Shoots a ray from the drawing point through the scene, testing for intersections
        /// with an object and its children, and returns a list of snap type and location pairs, if
        /// any exist.
        /// </summary>
        /// <param name="splinoViewport">The viewport to shoot the ray through</param>
        /// <param name="rayPosition">The start of the ray</param>
        /// <param name="rayDirection">The ray's direction</param>
        /// <returns>A list of snap type and location pairs</returns>
        public override List<ObjectSnap> SnapTest(SplinoViewport splinoViewport, Vector3 rayPosition, Vector3 rayDirection)
        {
            List<ObjectSnap> objectSnaps = new List<ObjectSnap>();
            ObjectSnap closeSplineSnap = new ObjectSnap();
            closeSplineSnap.ObjectSnapType = ObjectSnapType.None;

            objectSnaps.AddRange(base.SnapTest(splinoViewport, rayPosition, rayDirection));
            foreach (ObjectSnap objectSnap in objectSnaps)
            {
                if (objectSnap.SnapLocation == ((PointObject)_sceneChildren[0]).Center && _sceneChildren.Count > 2)
                {
                    closeSplineSnap.SnapLocation = objectSnap.SnapLocation;
                    closeSplineSnap.ObjectSnapType = ObjectSnapType.CloseSpline;
                    CloseSpline();
                }
            }
            _activeCloseSplineSnap = closeSplineSnap;

            if (_sceneChildren.Count > 2 && closeSplineSnap.ObjectSnapType == ObjectSnapType.CloseSpline)
                objectSnaps.Add(closeSplineSnap);
            _activeObjectSnaps = objectSnaps;
            return objectSnaps;
        }
    }
}
